package com.yueyang.sojourn.server.service.sojourn.merch.inventory;

import com.yueyang.sojourn.server.common.Services;
import com.yueyang.sojourn.server.common.baymax.web.result.JAssert;
import com.yueyang.sojourn.server.config.RetryConfig;
import com.yueyang.sojourn.server.model.enums.StatusEnum;
import com.yueyang.sojourn.server.model.po.SojournMerch;
import com.yueyang.sojourn.server.model.po.SojournMerchInventory;
import com.yueyang.sojourn.server.model.po.SojournOrderInventory;
import com.yueyang.sojourn.server.service.sojourn.merch.SojournMerchService;
import com.yueyang.sojourn.server.service.sojourn.order.inventory.SojournOrderInventoryService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.Nullable;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * @author yemingxing
 * @summary 商品库存管理 service
 * @Copyright (c) 2020, Yueyang Group All Rights Reserved.
 * @since 2020-03-29 22:34
 */
@Service
@Slf4j
public class MerchInventoryService {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private SojournMerchService sojournMerchService;
    @Autowired
    private SojournOrderInventoryService sojournOrderInventoryService;
    @Autowired
    private SojournMerchInventoryService sojournMerchInventoryService;
    @Resource(name = RetryConfig.SIMPLE_RETRY)
    private RetryTemplate simpleRetryTemplate;
    @Resource(name = RetryConfig.ALWAYS_RETRY)
    private RetryTemplate alwaysRetryTemplate;

    /**
     * 计算商品生效日期内的每天库存信息
     *
     * @param merchId 商品id
     * @return 日期及对应剩余库存数量
     */
    public Map<Long, Integer> calcMerchInventory(long merchId) {
        SojournMerch sojournMerch = sojournMerchService.selectByPrimaryKey(merchId);
        if (Objects.isNull(sojournMerch) || sojournMerch.getStatus() != StatusEnum.VALID.getCode()) {
            log.error("商品id无效:" + merchId);
            JAssert.justFailed("商品已无效");
        }
        // 取出每天的库存，并向后多取[sojournMerch.getNights() - 1]晚的库存  todo-ymx 可用日期最后一天的库存默认就是当天的库存吗？同一个项目的商品之间会否相互影响库存？
        Date effectDateEnd = DateUtils.addDays(sojournMerch.getAvailableDateEnd(), sojournMerch.getNights() - 1);
        List<SojournMerchInventory> inventories = sojournMerchInventoryService.selectByMerchIdAndEffectDate(merchId, sojournMerch.getAvailableDateBegin(), effectDateEnd);
        Map<Long, Integer> dayInventory = inventories.stream().collect(Collectors.toMap(invent -> invent.getEffectDate().getTime(), SojournMerchInventory::getInventory, (oldOne, newOne) -> newOne));

        return dayInventory.keySet().parallelStream().collect(Collectors.toMap(aLong -> aLong, aLong ->
                // 取这几晚库存的最小值，即为当天实际可出售的库存
                IntStream.range(0, sojournMerch.getNights()).parallel().mapToObj(i ->
                        dayInventory.get(aLong + TimeUnit.DAYS.toMillis(i)))
                        .filter(Objects::nonNull)
                        .map(l -> Math.max(0, l))
                        .min(Integer::compareTo).orElse(sojournMerch.getInventory())));
    }

    /**
     * 计算[生效日期]当天的剩余库存
     *
     * @param merchId    商品id
     * @param effectDate 生效日期
     * @param realNights 实际出行天数(非必传，默认取商品设定的出行晚数)
     * @return 剩余库存数量
     */
    public int calcRemnantInventory(long merchId, Date effectDate, @Nullable Integer realNights) {
        SojournMerch sojournMerch = sojournMerchService.selectByPrimaryKey(merchId);
        if (Objects.isNull(sojournMerch) || sojournMerch.getStatus() != StatusEnum.VALID.getCode()) {
            log.error("商品id无效:" + merchId);
            JAssert.justFailed("商品已无效");
        }
        // 默认取商品设定的出行晚数
        Integer nights = ObjectUtils.defaultIfNull(realNights, sojournMerch.getNights());
        List<SojournMerchInventory> inventories = this.getSojournMerchInventories(merchId, effectDate, nights);
        // 取这几晚库存的最小值，即为当天实际可出售的库存
        return inventories.stream().map(SojournMerchInventory::getInventory).map(l -> Math.max(0, l)).min(Integer::compareTo).orElse(0);
    }

    /**
     * 取库存
     *
     * @param orderSn    订单id
     * @param merchId    商品id
     * @param effectDate 生效日期(订单出行日期)
     * @param realNights 实际出行天数(非必传，默认取商品设定的出行晚数)
     * @param number     单日库存占用数量(这一订单的出行人数)
     */
    public void takeInventory(long creator, String orderSn, long merchId, Date effectDate, @Nullable Integer realNights, int number) {
        if (BooleanUtils.isFalse(redisTemplate.opsForValue().setIfAbsent(String.join(":", "sojourn:inventory:take", String.valueOf(merchId), String.valueOf(creator), DateFormatUtils.format(effectDate, "yyyy-MM-dd")), creator, 10, TimeUnit.SECONDS))) {
            // 10s内防重复提交
            return;
        }

        SojournMerch sojournMerch = sojournMerchService.selectByPrimaryKey(merchId);
        if (Objects.isNull(sojournMerch) || sojournMerch.getStatus() != StatusEnum.VALID.getCode()) {
            log.error("商品id无效:" + merchId);
            JAssert.justFailed("商品已无效");
        }
        // 默认取商品设定的出行晚数
        Integer nights = ObjectUtils.defaultIfNull(realNights, sojournMerch.getNights());
        List<SojournMerchInventory> merchInventories = this.getSojournMerchInventories(merchId, effectDate, nights);
        // 取这几晚库存的最小值
        Integer min = merchInventories.stream().map(SojournMerchInventory::getInventory).min(Integer::compareTo).orElse(0);
        if (min < number) {
            JAssert.justFailed("库存不足，请重新选择");
        }

        simpleRetryTemplate.execute(context -> {
            log.info("重试次数:" + context.getRetryCount());
            return Services.of(MerchInventoryService.class).batchTakeInventories(creator, orderSn, merchId, effectDate, nights, number, merchInventories);
        });
    }

    @Transactional(rollbackFor = Exception.class)
    public boolean batchTakeInventories(long creator, String orderSn, long merchId, Date effectDate, int nights,
                                        int number, List<SojournMerchInventory> merchInventories) {
        List<SojournOrderInventory> orderInventories = IntStream.range(0, nights).parallel().mapToObj(i -> {
            SojournOrderInventory inventory = new SojournOrderInventory();
            inventory.setOrderSn(orderSn);
            inventory.setMerchId(merchId);
            inventory.setEffectDate(DateUtils.addDays(effectDate, i));
            inventory.setNumber(number);
            inventory.setStatus(1);
            inventory.setCreator(creator);
            inventory.setCreatedTime(new Date());
            inventory.setUpdator(creator);
            inventory.setUpdatedTime(new Date());
            return inventory;
        }).collect(Collectors.toList());
        sojournOrderInventoryService.batchInsert(orderInventories);
        boolean result = merchInventories.stream()
                .allMatch(merchInventory -> sojournMerchInventoryService.takeInventory(merchInventory.getId(), number, merchInventory.getVersion()));
        if (!result) {
            JAssert.justFailed("库存不足，请重新选择");
        }
        return result;
    }


    /**
     * 查询出行日期内商品每天的库存情况
     *
     * @param merchId    商品id
     * @param effectDate 生效日期(出发日期)
     * @param nights     出行天数
     * @return
     */
    private List<SojournMerchInventory> getSojournMerchInventories(long merchId, Date effectDate, Integer nights) {
        Assert.notNull(nights, "nights 不能为空");
        Assert.isTrue(nights >= 1, "nights 不能小于1");
        // 查看当天及当天后[nights-1]晚的库存情况
        Date effectDateEnd = DateUtils.addDays(effectDate, nights - 1);
        return sojournMerchInventoryService.selectByMerchIdAndEffectDate(merchId, effectDate, effectDateEnd);
    }

    /**
     * 还库存
     *
     * @param merchId    商品id
     * @param effectDate 生效日期
     * @return 还回库存数量
     */
    public void giveBackInventory(long updator, String orderSn, long merchId, Date effectDate, int number) {
        if (BooleanUtils.isFalse(redisTemplate.opsForValue().setIfAbsent(String.join(":", "sojourn:inventory:back", String.valueOf(merchId), String.valueOf(updator), DateFormatUtils.format(effectDate, "yyyy-MM-dd")), updator, 10, TimeUnit.SECONDS))) {
            // 简单的加了个锁，避免重复还库存
            return;
        }
        SojournMerch sojournMerch = sojournMerchService.selectByPrimaryKey(merchId);
        if (Objects.isNull(sojournMerch) || sojournMerch.getStatus() != StatusEnum.VALID.getCode()) {
            log.error("商品id无效:" + merchId);
            JAssert.justFailed("商品已无效");
        }

        // 默认取商品设定的出行晚数
        Integer nights = sojournMerch.getNights();

        alwaysRetryTemplate.execute(context -> {
            log.info("重试次数:" + context.getRetryCount());
            List<SojournMerchInventory> merchInventories = this.getSojournMerchInventories(merchId, effectDate, nights);
            return Services.of(MerchInventoryService.class).batchGiveBackInventories(number, merchInventories);
        });
    }

    @Transactional(rollbackFor = Exception.class)
    public boolean batchGiveBackInventories(int number, List<SojournMerchInventory> merchInventories) {
        boolean result = merchInventories.stream()
                .allMatch(merchInventory -> sojournMerchInventoryService.addInventory(merchInventory.getId(), number, merchInventory.getVersion()));
        if (!result) {
            throw new IllegalStateException("库存恢复异常");
        }
        return result;
    }

    /**
     * 初始化商品每日库存信息
     *
     * @param sojournMerch
     */
    public void initInventory(SojournMerch sojournMerch) {
        int days = Math.toIntExact(Math.floorDiv(sojournMerch.getAvailableDateEnd().getTime() - sojournMerch.getAvailableDateBegin().getTime(),
                TimeUnit.DAYS.toMillis(1)));
        List<SojournMerchInventory> merchInventories = IntStream.range(0, days + 1).parallel().mapToObj(i -> {
            SojournMerchInventory sojournMerchInventory = new SojournMerchInventory();
            sojournMerchInventory.setMerchId(sojournMerch.getId());
            sojournMerchInventory.setEffectDate(DateUtils.addDays(sojournMerch.getAvailableDateBegin(), i));
            sojournMerchInventory.setInventory(sojournMerch.getInventory());
            sojournMerchInventory.setStatus(StatusEnum.VALID.getCode());
            sojournMerchInventory.setVersion(1);
            return sojournMerchInventory;
        }).collect(Collectors.toList());
        sojournMerchInventoryService.batchInsert(merchInventories);
    }

    /**
     * @param merchBefore  更新前商品信息
     * @param sojournMerch 更新后商品信息
     * @summary 更新库存信息
     * @author yemingxing
     * @version v1
     * @since 2020-04-06
     */
    @Async
    public void updateMerchInventories(SojournMerch merchBefore, SojournMerch sojournMerch) {
        List<SojournMerchInventory> inventories = sojournMerchInventoryService.selectByMerchId(merchBefore.getId());
        if (CollectionUtils.isEmpty(inventories)) {
            throw new IllegalStateException("库存信息有误，需后台管理员确认！");
        }

        if (Objects.nonNull(sojournMerch.getInventory()) && !Objects.equals(merchBefore.getInventory(), sojournMerch.getInventory())) {
            int difference = Math.subtractExact(sojournMerch.getInventory(), merchBefore.getInventory());
            alwaysRetryTemplate.execute(context -> {
                log.info("重试次数:" + context.getRetryCount());
                List<SojournMerchInventory> merchInventories = sojournMerchInventoryService.selectByMerchId(merchBefore.getId());
                return Services.of(MerchInventoryService.class).batchGiveBackInventories(difference, merchInventories);
            });
        }

        Date inventoryDateBegin = inventories.stream().map(SojournMerchInventory::getEffectDate).min(Date::compareTo).orElse(null);
        Date inventoryDateEnd = inventories.stream().map(SojournMerchInventory::getEffectDate).max(Date::compareTo).orElse(null);
        if (Objects.isNull(inventoryDateBegin) || Objects.isNull(inventoryDateEnd)) {
            throw new IllegalStateException("库存信息有误，需后台管理员确认！");
        }

        if ((Objects.nonNull(sojournMerch.getAvailableDateBegin()) && inventoryDateBegin.compareTo(sojournMerch.getAvailableDateBegin()) != 0) ||
                (Objects.nonNull(sojournMerch.getAvailableDateEnd()) && inventoryDateEnd.compareTo(sojournMerch.getAvailableDateEnd()) != 0)) {
            int beginDifference = Math.toIntExact(Math.floorDiv(sojournMerch.getAvailableDateBegin().getTime() - inventoryDateBegin.getTime(),
                    TimeUnit.DAYS.toMillis(1)));
            if (beginDifference < 0) {
                List<SojournMerchInventory> merchInventories = IntStream.range(beginDifference, 0).parallel().mapToObj(i -> {
                    SojournMerchInventory sojournMerchInventory = new SojournMerchInventory();
                    sojournMerchInventory.setMerchId(sojournMerch.getId());
                    sojournMerchInventory.setEffectDate(DateUtils.addDays(inventoryDateBegin, i));
                    sojournMerchInventory.setInventory(sojournMerch.getInventory());
                    sojournMerchInventory.setStatus(StatusEnum.VALID.getCode());
                    sojournMerchInventory.setVersion(1);
                    return sojournMerchInventory;
                }).collect(Collectors.toList());
                sojournMerchInventoryService.batchInsert(merchInventories);
            }

            int endDifference = Math.toIntExact(Math.floorDiv(sojournMerch.getAvailableDateEnd().getTime() - inventoryDateEnd.getTime(),
                    TimeUnit.DAYS.toMillis(1)));
            if (endDifference > 0) {
                List<SojournMerchInventory> merchInventories = IntStream.range(1, endDifference + 1).parallel().mapToObj(i -> {
                    SojournMerchInventory sojournMerchInventory = new SojournMerchInventory();
                    sojournMerchInventory.setMerchId(sojournMerch.getId());
                    sojournMerchInventory.setEffectDate(DateUtils.addDays(inventoryDateEnd, i));
                    sojournMerchInventory.setInventory(sojournMerch.getInventory());
                    sojournMerchInventory.setStatus(StatusEnum.VALID.getCode());
                    sojournMerchInventory.setVersion(1);
                    return sojournMerchInventory;
                }).collect(Collectors.toList());
                sojournMerchInventoryService.batchInsert(merchInventories);
            }
        }
    }
}
